#region File Description
// CameraComponent
// An orbit camera based on ChaseCamera sample, adapted for XNAViewer demo
// Added viewport field, so we can do split viewports
#endregion

#region Using statments
using System;
using System.Collections.Generic;
using System.Linq;
using System.Diagnostics;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
using InputEventSystem;
#endregion

namespace XNAViewerSample
{
    #region Delegates
    public delegate void CameraHandler(GameTime time, KeyEventArgs key, MouseEventArgs mouse);
    #endregion

    /// <summary>
    /// This is a game component that implements IUpdateable.
    /// </summary>
    public class CameraComponent : Microsoft.Xna.Framework.GameComponent
    {
        #region Fields
        private float cameraArc;
        private float cameraRotation;
        private float cameraFOV;
        private float cameraAspectRatio;
        private float cameraNearPlaneDistance;
        private float cameraFarPlaneDistance;
        private Vector3 cameraPosition;
        private Vector3 cameraVelocity;

        private Matrix viewMatrix;
        private Matrix projectionMatrix;
        private Viewport viewport;
        private InputEvents input;

        // Chase camera specific
        // Chased object
        private Vector3 chasePosition;
        private Vector3 chaseDirection;
        private Vector3 chaseUp;
        private String chaseName;
        // Desired camera
        private Vector3 desiredPositionOffset;
        private Vector3 desiredPosition;
        private Vector3 lookAtOffset;
        private Vector3 lookAt;
        // Spring physics
        private float stiffness;
        private float damping;
        private float mass;
        private bool springEnabled;
        #endregion

        #region Events
        public event CameraHandler cameraHandler;
        private Keys[] validKeys = {   //Keys.Up,
                                       //Keys.Down,
                                       //Keys.Right,
                                       //Keys.Left,
                                       Keys.OemPipe,
                                   };
        private MouseEventArgs mouse;
        private KeyEventArgs keyPressed;

        private bool handlerUpdate;
        #endregion

        public CameraComponent(Game game)
            : base(game)
        {
            cameraArc = 0;
            cameraRotation = 0;
            cameraFOV = MathHelper.PiOver4;
            cameraPosition = Vector3.Zero;
            cameraAspectRatio = 4.0f / 3.0f;
            cameraNearPlaneDistance = 2.0f;
            cameraFarPlaneDistance = 5000.0f;
            input = null;

            // Chase camera specific
            chaseUp = Vector3.Up;
            desiredPositionOffset = new Vector3(0, 2.0f, 2.0f);
            lookAtOffset = new Vector3(0, 2.0f, 0);
            stiffness = 1800.0f;
            damping = 600.0f;
            mass = 50.0f;
            springEnabled = true;

            // Handler specific
            mouse = new MouseEventArgs();
            keyPressed = new KeyEventArgs();
        }

        /// <summary>
        /// Allows the game component to perform any initialization it needs to before starting
        /// to run.  This is where it can query for any required services and load content.
        /// </summary>
        public override void Initialize()
        {
            keyPressed.Key = Keys.Escape;
            handlerUpdate = true;
            base.Initialize();

            // Set the camera aspect ratio
            // This must be done after the call to base.Initalize() which will
            // initialize the graphics device.
            cameraAspectRatio = (float)viewport.Width / viewport.Height;

            // Initialize mouse position
            Rectangle clientBounds = this.Game.Window.ClientBounds;
            Mouse.SetPosition(clientBounds.Width / 2, clientBounds.Height / 2);

            // Perform an inital reset on the camera so that it starts at the resting
            // position. If we don't do this, the camera will start at the origin and
            // race across the world to get behind the chased object.
            // This is performed here because the aspect ratio is needed by Reset.
            //UpdateCameraChaseTarget();
            // At this point in initialization, a camera target is still non-
            // existent. Hence the camera will start at origin.
            Reset();

        }

        #region Public Get/Set Fields
        /// <summary>
        /// Sets the control class for the camera
        /// </summary>
        public InputEvents Controls
        {
            set
            {
                Debug.Assert(value != null);
                input = value;
                input.KeyDown += new KeyDownHandler(input_KeyDown);
                input.MouseMove += new MouseMoveHandler(input_MouseMove);
                input.MouseWheel += new MouseWheelHandler(input_MouseWheel);
                input.MouseUp += new MouseUpHandler(input_MouseUp);
                input.MouseDown += new MouseDownHandler(input_MouseDown);
            }
        }

        /// <summary>
        /// Gets camera view matrix
        /// </summary>
        public Matrix ViewMatrix
        {
            get { return viewMatrix; }
        }

        /// <summary>
        /// Gets camera projection matrix
        /// </summary>
        public Matrix ProjectionMatrix
        {
            get { return projectionMatrix; }
        }

        /// <summary>
        /// Gets/Sets the camera viewport
        /// </summary>
        public Viewport Viewport
        {
            get { return viewport; }
            set { viewport = value; }
        }

        /// <summary>
        /// Gets/Sets camera arc (rotation along Xaxis)
        /// </summary>
        public float Arc
        {
            get { return cameraArc; }
            set { cameraArc = MathHelper.ToDegrees(MathHelper.WrapAngle(MathHelper.ToRadians(value))); }
        }

        /// <summary>
        /// Gets/Sets camera rotation (rotation along Yaxis)
        /// </summary>
        public float Rotation
        {
            get { return cameraRotation; }
            set { cameraRotation = MathHelper.ToDegrees(MathHelper.WrapAngle(MathHelper.ToRadians(value))); }
        }

        /// <summary>
        /// Gets/Sets camera field of view
        /// </summary>
        public float FoV
        {
            get { return cameraFOV; }
            set { cameraFOV = value; }
        }

        /// <summary>
        /// Gets/Sets camera position in world space
        /// </summary>
        public Vector3 Position
        {
            get { return cameraPosition; }
            set { cameraPosition = value; }
        }

        // Chase camera specific
        /// <summary>
        /// Position of object being chased.
        /// </summary>
        public Vector3 ChasePosition
        {
            get { return chasePosition; }
            set { chasePosition = value; }
        }

        /// <summary>
        /// Direction the chased object is facing.
        /// </summary>
        public Vector3 ChaseDirection
        {
            get { return chaseDirection; }
            set { chaseDirection = value; }
        }

        /// <summary>
        /// Chased object's Up vector.
        /// </summary>
        public Vector3 ChaseUp
        {
            get { return chaseUp; }
            set { chaseUp = value; }
        }

        public String ChaseName
        {
            get { return chaseName; }
            set { chaseName = value; }
        }

        /// <summary>
        /// Desired camera position in the chased object's coordinate system.
        /// </summary>
        public Vector3 DesiredPositionOffset
        {
            get { return desiredPositionOffset; }
            set { desiredPositionOffset = value; }
        }

        /// <summary>
        /// Desired camera position in world space.
        /// </summary>
        public Vector3 DesiredPosition
        {
            get
            {
                UpdateWorldPositions();
                return desiredPosition;
            }
        }

        /// <summary>
        /// Look at point in the chased object's coordinate system.
        /// </summary>
        public Vector3 LookAtOffset
        {
            get { return lookAtOffset; }
            set { lookAtOffset = value; }
        }

        /// <summary>
        /// Look at point in world space.
        /// </summary>
        public Vector3 LookAt
        {
            get
            {
                UpdateWorldPositions();
                return lookAt;
            }
        }

        /// <summary>
        /// Physics coefficient which controls the influence of the camera's position
        /// over the spring force. The stiffer the spring, the closer it will stay to
        /// the chased object.
        /// </summary>
        public float Stiffness
        {
            get { return stiffness; }
            set { stiffness = value; }
        }

        /// <summary>
        /// Physics coefficient which approximates internal friction of the spring.
        /// Sufficient damping will prevent the spring from oscillating infinitely.
        /// </summary>
        public float Damping
        {
            get { return damping; }
            set { damping = value; }
        }

        /// <summary>
        /// Mass of the camera body. Heaver objects require stiffer springs with less
        /// damping to move at the same rate as lighter objects.
        /// </summary>
        public float Mass
        {
            get { return mass; }
            set { mass = value; }
        }

        /// <summary>
        /// Boolean flag to set/get if camera is spring enabled
        /// </summary>
        public bool SpringEnabled
        {
            get { return springEnabled; }
            set { springEnabled = value; }
        }

        /// <summary>
        /// Boolean flag to set/get if camera handler requires a force update
        /// </summary>
        public bool HandlerUpdate
        {
            get { return handlerUpdate; }
            set { handlerUpdate = value; }
        }

        /// <summary>
        /// Velocity of camera.
        /// </summary>
        public Vector3 Velocity
        {
            get { return cameraVelocity; }
        }

        /// <summary>
        /// Perspective aspect ratio. Default value should be overriden by application.
        /// </summary>
        public float AspectRatio
        {
            get { return cameraAspectRatio; }
            set { cameraAspectRatio = value; }
        }

        /// <summary>
        /// Distance to the near clipping plane.
        /// </summary>
        public float NearPlaneDistance
        {
            get { return cameraNearPlaneDistance; }
            set { cameraNearPlaneDistance = value; }
        }

        /// <summary>
        /// Distance to the far clipping plane.
        /// </summary>
        public float FarPlaneDistance
        {
            get { return cameraFarPlaneDistance; }
            set { cameraFarPlaneDistance = value; }
        }

        #endregion

        #region Callbacks and Handlers
        /// <summary>
        /// Callback for keyboard down event
        /// </summary>
        /// <param name="args">Key event argument</param>
        /// <param name="time">Elapsed game time</param>
        private void input_KeyDown(KeyEventArgs args, GameTime time)
        {
            if (validKeys.Contains(args.Key))
                keyPressed = args;
            else
                keyPressed.Key = Keys.Escape;

            handlerUpdate = true;
        }

        /// <summary>
        /// Callback for mouse down event
        /// This forces the mouse to move to center when camera button
        /// (or middle mouse button) is clicked
        /// TODO: maybe change cursor pointer to indicate camera handling
        /// </summary>
        /// <param name="args">Mouse event argument</param>
        void input_MouseDown(MouseEventArgs args, GameTime time)
        {
            if ((args.State.X < 0) ||
                (args.State.Y < 0) ||
                (args.State.X > this.Game.Window.ClientBounds.Width) ||
                (args.State.Y > this.Game.Window.ClientBounds.Height))
                return;

            if (//(args.Button == MouseButtons.Left) ||
                (args.Button == MouseButtons.Middle) ||
                (args.Button == MouseButtons.Right))
            {
                Rectangle clientBounds = this.Game.Window.ClientBounds;

                int centerX = clientBounds.Width / 2;
                int centerY = clientBounds.Height / 2;
                Mouse.SetPosition(centerX, centerY);
                this.Game.IsMouseVisible = false;
            }
        }

        /// <summary>
        /// Callback for mouse up event
        /// </summary>
        /// <param name="args"></param>
        /// <param name="time"></param>
        void input_MouseUp(MouseEventArgs args, GameTime time)
        {
            if ((args.State.X < 0) ||
                (args.State.Y < 0) ||
                (args.State.X > this.Game.Window.ClientBounds.Width) ||
                (args.State.Y > this.Game.Window.ClientBounds.Height))
                return;

            if (//(args.Button == MouseButtons.Left) ||
                (args.Button == MouseButtons.Middle) ||
                (args.Button == MouseButtons.Right))
            {
                // restore cursor here
                this.Game.IsMouseVisible = true;
            }
        }

        /// <summary>
        /// Callback for mouse movement event
        /// </summary>
        /// <param name="args">Mouse event argument</param>
        void input_MouseMove(MouseEventArgs args, GameTime time)
        {
            if ((args.State.X < 0) ||
                (args.State.Y < 0) ||
                (args.State.X > this.Game.Window.ClientBounds.Width) ||
                (args.State.Y > this.Game.Window.ClientBounds.Height))
                return;

            if (//(args.State.LeftButton == ButtonState.Pressed) ||
                (args.State.MiddleButton == ButtonState.Pressed) ||
                (args.State.RightButton == ButtonState.Pressed))
            {
                Rectangle clientBounds = this.Game.Window.ClientBounds;

                int centerX = clientBounds.Width / 2;
                int centerY = clientBounds.Height / 2;

                //mouse = args;
                mouse.State = args.State;
                if (mouse.State.LeftButton == ButtonState.Pressed)
                    mouse.Button = MouseButtons.Left;
                if (mouse.State.MiddleButton == ButtonState.Pressed)
                    mouse.Button = MouseButtons.Middle;
                if (mouse.State.RightButton == ButtonState.Pressed)
                    mouse.Button = MouseButtons.Right;
                mouse.Position.X = centerX - args.Position.X;
                mouse.Position.Y = centerY - args.Position.Y;

                // Reset mouse position
                Mouse.SetPosition(centerX, centerY);

                handlerUpdate = true;
            }
        }

        /// <summary>
        /// Callback for mouse wheel event
        /// </summary>
        /// <param name="args">Mouse event argument</param>
        void input_MouseWheel(MouseEventArgs args, GameTime time)
        {
            mouse.Button = MouseButtons.None;
            mouse.State = args.State;
            mouse.Scroll = args.Scroll;
            handlerUpdate = true;
        }

        #endregion

        #region Update Methods

        /// <summary>
        /// Forces camera to be at desired position and to stop moving. The is useful
        /// when the chased object is first created or after it has been teleported.
        /// Failing to call this after a large change to the chased object's position
        /// will result in the camera quickly flying across the world.
        /// </summary>
        public void Reset()
        {
            UpdateWorldPositions();

            cameraVelocity = Vector3.Zero;
            cameraPosition = desiredPosition;

            UpdateMatrices();
        }

        /// <summary>
        /// Rebuilds camera's view and projection matricies.
        /// </summary>
        private void UpdateMatrices()
        {
            viewMatrix = Matrix.CreateLookAt(this.Position, this.LookAt, this.ChaseUp);
            projectionMatrix = Matrix.CreatePerspectiveFieldOfView(cameraFOV,
                                                                    cameraAspectRatio,
                                                                    cameraNearPlaneDistance,
                                                                    cameraFarPlaneDistance);
        }

        /// <summary>
        /// Rebuilds object space values in world space. Invoke before publicly
        /// returning or privately accessing world space values.
        /// </summary>
        private void UpdateWorldPositions()
        {
            // Construct a matrix to transform from object space to worldspace
            Matrix transform = Matrix.Identity;
            transform.Backward = ChaseDirection;
            transform.Up = ChaseUp;
            transform.Right = Vector3.Cross(ChaseUp, ChaseDirection);

            // Calculate desired camera properties in world space
            desiredPosition = ChasePosition + Vector3.TransformNormal(DesiredPositionOffset, transform);
            lookAt = ChasePosition + Vector3.TransformNormal(LookAtOffset, transform);
        }


        /// <summary>
        /// Allows the game component to update itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        public override void Update(GameTime gameTime)
        {
            if ((cameraHandler != null) && (handlerUpdate == true))
            {
                cameraHandler(gameTime, keyPressed, mouse);
                keyPressed.Key = Keys.Escape;
                handlerUpdate = false;
            }

            if (springEnabled)
            {
                UpdateWorldPositions();

                float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;

                // Calculate spring force
                Vector3 stretch = cameraPosition - desiredPosition;
                Vector3 force = -stiffness * stretch - damping * cameraVelocity;
                // Apply acceleration
                Vector3 acceleration = force / mass;
                cameraVelocity += acceleration * elapsed;
                // Apply velocity
                cameraPosition += cameraVelocity * elapsed;

                UpdateMatrices();
            }
            else
            {
                Reset();
            }

            base.Update(gameTime);
        }
        #endregion
    }
}